﻿/*
 * Copyright (c) 2015-2016 Constantijn Evenhuis (blueprints@const.nl)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Blueprints.IO;

namespace Blueprints
{
    partial class BlueprintContext
    {
        /// <summary>
        /// Deserializes a blueprint.
        /// </summary>
        /// <typeparam name="TargetType">The type of object to deserialize.</typeparam>
        /// <param name="reader">The reader from which to read.</param>
        /// <param name="id">The id of node containing the object to deserialize.</param>
        /// <returns>The deserialized object.</returns>
        public TargetType Deserialize<TargetType>(IBlueprintReader reader, string id = null)
        {
            if (reader == null) throw new ArgumentNullException(nameof(reader));
            using (NodeInfoReader nodeReader = new NodeInfoReader(reader))
            {
                if (string.IsNullOrEmpty(id))
                {
                    if (!reader.MoveToStartNode())
                    {
                        throw new BlueprintException("Document has no valid content");
                    }
                }
                else
                {
                    if (!reader.MoveToId(id))
                    {
                        throw new BlueprintException("Document does not contain a node with the requested id '" + id + "'");
                    }
                }

                return (TargetType)Deserialize(nodeReader, typeof(TargetType));
            }
        }

        /// <summary>
        /// Deserializes a blueprint.
        /// </summary>
        /// <typeparam name="TargetType">The type of object to deserialize.</typeparam>
        /// <param name="stream">The stream from which to read.</param>
        /// <param name="uri">The uri at which the blueprint is located. This is used to resolve relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        /// <returns>The deserialized object.</returns>
        public TargetType Deserialize<TargetType>(Stream stream, Uri uri = null, MediaType mediaType = MediaType.Unknown)
        {
            if (stream == null) throw new ArgumentNullException(nameof(stream));
            if (!stream.CanRead) throw new ArgumentException("The provided stream must be readable", nameof(stream));
            if (uri == null)
            {
                uri = new Uri("this:");
            }
            else if (mediaType == MediaType.Unknown)
            {
                mediaType = uri.GetMediaType();
            }

            using (IBlueprintReader reader = BlueprintResolver.CreateReader(stream, uri, mediaType))
            {
                return (TargetType)Deserialize(reader, typeof(TargetType));
            }
        }

        /// <summary>
        /// Deserializes a blueprint.
        /// </summary>
        /// <typeparam name="TargetType">The type of object to deserialize.</typeparam>
        /// <param name="uri">The uri at which the blueprint is located.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        /// <returns>The deserialized object.</returns>
        public TargetType Deserialize<TargetType>(Uri uri, MediaType mediaType = MediaType.Unknown)
        {
            if (uri == null) throw new ArgumentNullException(nameof(uri));
            MediaType uriMediaType;
            using (Stream stream = uri.GetStream(out uriMediaType))
            {
                if (mediaType == MediaType.Unknown)
                {
                    mediaType = uriMediaType;
                }

                using (IBlueprintReader reader = BlueprintResolver.CreateReader(stream, uri, mediaType))
                {
                    return (TargetType)Deserialize(reader, typeof(TargetType), uri.GetFragment());
                }
            }
        }

        /// <summary>
        /// Deserializes a blueprint, and assigns its values to <paramref name="instance"/>.
        /// </summary>
        /// <param name="type">The type of object of which to deserialize properties.</param>
        /// <param name="instance">The object instance to assign the values to.</param>
        /// <param name="reader">The reader from which to read.</param>
        /// <param name="id">The id of node containing the object to deserialize.</param>
        public void DeserializeProperties(Type type, object instance, IBlueprintReader reader, string id = null)
        {
            if (type == null) throw new ArgumentNullException(nameof(type));
            if (instance == null) throw new ArgumentNullException(nameof(instance));
            if (reader == null) throw new ArgumentNullException(nameof(reader));
            if (!type.GetTypeInfo().IsAssignableFrom(instance.GetType().GetTypeInfo()))
                throw new ArgumentException("'instance' must be compatible with type '" + type.Name + "'", nameof(instance));

            using (NodeInfoReader nodeReader = new NodeInfoReader(reader))
            {
                if (string.IsNullOrEmpty(id))
                {
                    if (!nodeReader.MoveToStartNode())
                    {
                        throw new BlueprintException("Document has no valid content");
                    }
                }
                else
                {
                    if (!nodeReader.MoveToId(id))
                    {
                        throw new BlueprintException("Document does not contain a node with the requested id '" + id + "'");
                    }
                }

                DeserializePropertiesInternal(type, instance, nodeReader);
            }
        }

        /// <summary>
        /// Deserializes a blueprint, and assigns its values to <paramref name="instance"/>.
        /// </summary>
        /// <param name="instance">The object instance to assign the values to.</param>
        /// <param name="reader">The reader from which to read.</param>
        /// <param name="id">The id of node containing the object to deserialize.</param>
        public void DeserializeProperties(object instance, IBlueprintReader reader, string id = null)
        {
            if (instance == null) throw new ArgumentNullException(nameof(instance));
            DeserializeProperties(instance.GetType(), instance, reader, id);
        }

        /// <summary>
        /// Deserializes a blueprint, and assigns its values to <paramref name="instance"/>.
        /// </summary>
        /// <param name="type">The type of object of which to deserialize properties.</param>
        /// <param name="instance">The object instance to assign the values to.</param>
        /// <param name="stream">The stream from which to read.</param>
        /// <param name="uri">The uri at which the blueprint is located. This is used to resolve relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        public void DeserializeProperties(Type type, object instance, Stream stream, Uri uri = null, MediaType mediaType = MediaType.Unknown)
        {
            if (stream == null) throw new ArgumentNullException(nameof(stream));
            if (!stream.CanRead) throw new ArgumentException("The provided stream must be readable", nameof(stream));
            if (uri == null)
            {
                uri = new Uri("this:");
            }
            else if (mediaType == MediaType.Unknown)
            {
                mediaType = uri.GetMediaType();
            }

            using (IBlueprintReader reader = BlueprintResolver.CreateReader(stream, uri, mediaType))
            {
                DeserializeProperties(type, instance, reader);
            }
        }

        /// <summary>
        /// Deserializes a blueprint, and assigns its values to <paramref name="instance"/>.
        /// </summary>
        /// <param name="instance">The object instance to assign the values to.</param>
        /// <param name="stream">The stream from which to read.</param>
        /// <param name="uri">The uri at which the blueprint is located. This is used to resolve relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        public void DeserializeProperties(object instance, Stream stream, Uri uri = null, MediaType mediaType = MediaType.Unknown)
        {
            if (instance == null) throw new ArgumentNullException(nameof(instance));
            DeserializeProperties(instance.GetType(), instance, stream, uri, mediaType);
        }

        /// <summary>
        /// Deserializes a blueprint, and assigns its values to <paramref name="instance"/>.
        /// </summary>
        /// <param name="type">The type of object of which to deserialize properties.</param>
        /// <param name="instance">The object instance to assign the values to.</param>
        /// <param name="uri">The uri at which the blueprint is located.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        public void DeserializeProperties(Type type, object instance, Uri uri, MediaType mediaType = MediaType.Unknown)
        {
            if (uri == null) throw new ArgumentNullException(nameof(uri));
            MediaType uriMediaType;
            using (Stream stream = BlueprintResolver.CreateReaderStream(uri, out uriMediaType))
            {
                if (mediaType == MediaType.Unknown)
                {
                    mediaType = uriMediaType;
                }

                using (IBlueprintReader reader = BlueprintResolver.CreateReader(stream, uri, mediaType))
                {
                    DeserializeProperties(type, instance, reader, uri.GetFragment());
                }
            }
        }

        /// <summary>
        /// Deserializes a blueprint, and assigns its values to <paramref name="instance"/>.
        /// </summary>
        /// <param name="instance">The object instance to assign the values to.</param>
        /// <param name="uri">The uri at which the blueprint is located.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        public void DeserializeProperties(object instance, Uri uri, MediaType mediaType = MediaType.Unknown)
        {
            if (instance == null) throw new ArgumentNullException(nameof(instance));
            DeserializeProperties(instance.GetType(), instance, uri, mediaType);
        }

        /// <summary>
        /// Deserializes a blueprint, and assigns its values to <paramref name="instance"/>.
        /// </summary>
        /// <typeparam name="TargetType">The type of object of which to deserialize properties.</typeparam>
        /// <param name="instance">The object instance to assign the values to.</param>
        /// <param name="reader">The reader from which to read.</param>
        /// <param name="id">The id of node containing the object to deserialize.</param>
        public void DeserializeProperties<TargetType>(TargetType instance, IBlueprintReader reader, string id = null)
        {
            DeserializeProperties(typeof(TargetType), instance, reader, id);
        }

        /// <summary>
        /// Deserializes a blueprint, and assigns its values to <paramref name="instance"/>.
        /// </summary>
        /// <typeparam name="TargetType">The type of object of which to deserialize properties.</typeparam>
        /// <param name="instance">The object instance to assign the values to.</param>
        /// <param name="stream">The stream from which to read.</param>
        /// <param name="uri">The uri at which the blueprint is located. This is used to resolve relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        public void DeserializeProperties<TargetType>(TargetType instance, Stream stream, Uri uri = null, MediaType mediaType = MediaType.Unknown)
        {
            DeserializeProperties(typeof(TargetType), instance, stream, uri, mediaType);
        }

        /// <summary>
        /// Deserializes a blueprint, and assigns its values to <paramref name="instance"/>.
        /// </summary>
        /// <typeparam name="TargetType">The type of object of which to deserialize properties.</typeparam>
        /// <param name="instance">The object instance to assign the values to.</param>
        /// <param name="uri">The uri at which the blueprint is located.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        public void DeserializeProperties<TargetType>(TargetType instance, Uri uri, MediaType mediaType = MediaType.Unknown)
        {
            DeserializeProperties(typeof(TargetType), instance, uri, mediaType);
        }

        /// <summary>
        /// Deserializes multiple items of the same type from a blueprint.
        /// </summary>
        /// <param name="reader">The reader from which to read.</param>
        /// <param name="itemType">The type of items to deserialize.</param>
        /// <param name="id">The id of node containing the items to deserialize.</param>
        /// <returns>The deserialized items.</returns>
        public IEnumerable DeserializeItems(IBlueprintReader reader, Type itemType = null, string id = null)
        {
            if (reader == null) throw new ArgumentNullException(nameof(reader));
            using (NodeInfoReader nodeReader = new NodeInfoReader(reader))
            {
                if (string.IsNullOrEmpty(id))
                {
                    if (!nodeReader.MoveToStartNode())
                    {
                        throw new BlueprintException("Document has no valid content");
                    }
                }
                else
                {
                    if (!nodeReader.MoveToId(id))
                    {
                        throw new BlueprintException("Document does not contain a node with the requested id '" + id + "'");
                    }
                }

                return DeserializeItems(nodeReader, itemType);
            }
        }

        /// <summary>
        /// Deserializes multiple items of the same type from a blueprint.
        /// </summary>
        /// <param name="stream">The stream from which to read.</param>
        /// <param name="itemType">The type of items to deserialize.</param>
        /// <param name="uri">The uri at which the blueprint is located. This is used to resolve relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        /// <returns>The deserialized items.</returns>
        public IEnumerable DeserializeItems(Stream stream, Type itemType = null, Uri uri = null, MediaType mediaType = MediaType.Unknown)
        {
            if (stream == null) throw new ArgumentNullException(nameof(stream));
            if (!stream.CanRead) throw new ArgumentException("The provided stream must be readable", nameof(stream));
            if (uri == null)
            {
                uri = new Uri("this:");
            }
            else if (mediaType == MediaType.Unknown)
            {
                mediaType = uri.GetMediaType();
            }

            using (IBlueprintReader reader = BlueprintResolver.CreateReader(stream, uri, mediaType))
            {
                foreach (object item in DeserializeItems(reader, itemType))
                {
                    yield return item;
                }
            }
        }

        /// <summary>
        /// Deserializes items from a blueprint.
        /// </summary>
        /// <param name="uri">The uri at which the blueprint is located.</param>
        /// <param name="itemType">The type of items to deserialize.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        /// <returns>The deserialized items.</returns>
        public IEnumerable DeserializeItems(Uri uri, Type itemType = null, MediaType mediaType = MediaType.Unknown)
        {
            if (uri == null) throw new ArgumentNullException(nameof(uri));
            MediaType uriMediaType;
            using (Stream stream = BlueprintResolver.CreateReaderStream(uri, out uriMediaType))
            {
                if (mediaType == MediaType.Unknown)
                {
                    mediaType = uriMediaType;
                }

                using (IBlueprintReader reader = BlueprintResolver.CreateReader(stream, uri, mediaType))
                {
                    return DeserializeItems(reader, itemType, uri.GetFragment());
                }
            }
        }

        /// <summary>
        /// Deserializes multiple items of the same type from a blueprint.
        /// </summary>
        /// <typeparam name="ItemType">The type of items to deserialize.</typeparam>
        /// <param name="reader">The reader from which to read.</param>
        /// <param name="id">The id of node containing the items to deserialize.</param>
        /// <returns>The deserialized items.</returns>
        public IEnumerable<ItemType> DeserializeItems<ItemType>(IBlueprintReader reader, string id = null)
        {
            if (reader == null) throw new ArgumentNullException(nameof(reader));
            foreach (object item in DeserializeItems(reader, typeof(ItemType), id))
            {
                yield return (ItemType)item;
            }
        }

        /// <summary>
        /// Deserializes multiple items of the same type from a blueprint.
        /// </summary>
        /// <typeparam name="ItemType">The type of items to deserialize.</typeparam>
        /// <param name="stream">The stream from which to read.</param>
        /// <param name="uri">The uri at which the blueprint is located. This is used to resolve relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        /// <returns>The deserialized items.</returns>
        public IEnumerable<ItemType> DeserializeItems<ItemType>(Stream stream, Uri uri = null, MediaType mediaType = MediaType.Unknown)
        {
            if (stream == null) throw new ArgumentNullException(nameof(stream));
            if (!stream.CanRead) throw new ArgumentException("The provided stream must be readable", nameof(stream));
            foreach (object item in DeserializeItems(stream, typeof(ItemType), uri, mediaType))
            {
                yield return (ItemType)item;
            }
        }

        /// <summary>
        /// Deserializes items from a blueprint.
        /// </summary>
        /// <typeparam name="ItemType">The type of items to deserialize.</typeparam>
        /// <param name="uri">The uri at which the blueprint is located.</param>
        /// <param name="mediaType">The media type with which the blueprint is stored.</param>
        /// <returns>The deserialized items.</returns>
        public IEnumerable<ItemType> DeserializeItems<ItemType>(Uri uri, MediaType mediaType = MediaType.Unknown)
        {
            if (uri == null) throw new ArgumentNullException(nameof(uri));
            foreach (object item in DeserializeItems(uri, typeof(ItemType), mediaType))
            {
                yield return (ItemType)item;
            }
        }

        /// <summary>
        /// Serializes an object.
        /// </summary>
        /// <param name="writer">The writer to write to.</param>
        /// <param name="instance">The object instance to serialize.</param>
        /// <param name="rootName">The name of the root node.</param>
        public void Serialize(IBlueprintWriter writer, object instance, string rootName = null)
        {
            if (writer == null) throw new ArgumentNullException(nameof(writer));
            foreach (INode node in Serialize(instance, writer.DocumentUri, rootName))
            {
                writer.Write(node);
            }
        }

        /// <summary>
        /// Serializes an object.
        /// </summary>
        /// <param name="instance">The object instance to serialize.</param>
        /// <param name="uri">The uri at which the blueprint is to be stored. This is used to generate relative links within the blueprint.</param>
        /// <returns>The serialized nodes.</returns>
        /// <param name="rootName">The name of the root node.</param>
        public IReadOnlyList<INode> Serialize(object instance, Uri uri = null, string rootName = null)
        {
            if (uri == null)
            {
                uri = new Uri("this:");
            }

            using (SerializationScope scope = new SerializationScope(this, uri))
            {
                Serialize(scope, instance, typeof(object), rootName ?? "BlueprintObject");
                return scope.Nodes;
            }
        }

        /// <summary>
        /// Serializes an object.
        /// </summary>
        /// <param name="stream">The stream to write to.</param>
        /// <param name="instance">The object instance to serialize.</param>
        /// <param name="uri">The uri at which the blueprint is to be stored. This is used to generate relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which to store the blueprint.</param>
        /// <param name="rootName">The name of the root node.</param>
        public void Serialize(Stream stream, object instance, Uri uri = null, MediaType mediaType = MediaType.Unknown, string rootName = null)
        {
            if (stream == null) throw new ArgumentNullException(nameof(stream));
            if (!stream.CanWrite) throw new ArgumentException("The provided stream must be writeable", nameof(stream));
            if (uri == null)
            {
                uri = new Uri("this:");
            }
            else if (mediaType == MediaType.Unknown)
            {
                mediaType = uri.GetMediaType();
            }

            using (IBlueprintWriter writer = BlueprintResolver.CreateWriter(stream, uri, mediaType))
            {
                Serialize(writer, instance, rootName);
            }
        }

        /// <summary>
        /// Serializes the properties of an object.
        /// </summary>
        /// <param name="writer">The writer to write to.</param>
        /// <param name="targetType">The type of which to serialize properties.</param>
        /// <param name="instance">The object of which to serialize properties.</param>
        /// <param name="rootName">The name of the root node.</param>
        public void SerializeProperties(IBlueprintWriter writer, Type targetType, object instance, string rootName = null)
        {
            if (writer == null) throw new ArgumentNullException(nameof(writer));
            foreach (INode node in SerializeProperties(targetType, instance, writer.DocumentUri, rootName))
            {
                writer.Write(node);
            }
        }

        /// <summary>
        /// Serializes the properties of an object.
        /// </summary>
        /// <param name="writer">The writer to write to.</param>
        /// <param name="instance">The object of which to serialize properties.</param>
        /// <param name="rootName">The name of the root node.</param>
        public void SerializeProperties(IBlueprintWriter writer, object instance, string rootName = null)
        {
            if (instance == null) throw new ArgumentNullException(nameof(instance));
            SerializeProperties(writer, instance.GetType(), instance, rootName);
        }

        /// <summary>
        /// Serializes the properties of an object.
        /// </summary>
        /// <typeparam name="TargetType">The type of which to serialize properties.</typeparam>
        /// <param name="writer">The writer to write to.</param>
        /// <param name="instance">The object of which to serialize properties.</param>
        /// <param name="rootName">The name of the root node.</param>
        public void SerializeProperties<TargetType>(IBlueprintWriter writer, TargetType instance, string rootName = null)
        {
            SerializeProperties(writer, typeof(TargetType), instance, rootName);
        }

        /// <summary>
        /// Serializes the properties of an object.
        /// </summary>
        /// <param name="targetType">The type of which to serialize properties.</param>
        /// <param name="instance">The object of which to serialize properties.</param>
        /// <param name="uri">The uri at which the blueprint is to be stored. This is used to generate relative links within the blueprint.</param>
        /// <param name="rootName">The name of the root node.</param>
        /// <returns>The serialized nodes.</returns>
        public IReadOnlyList<INode> SerializeProperties(Type targetType, object instance, Uri uri = null, string rootName = null)
        {
            if (instance == null) throw new ArgumentNullException(nameof(instance));
            if (!targetType.GetTypeInfo().IsAssignableFrom(instance.GetType().GetTypeInfo()))
                throw new ArgumentException("'instance' must be compatible with type '" + targetType.Name + "'", nameof(instance));

            if (uri == null)
            {
                uri = new Uri("this:");
            }

            using (SerializationScope scope = new SerializationScope(this, uri))
            {
                scope.Nodes.Add(new StartNode(rootName ?? "BlueprintProperties", null, null, null));
                SerializeProperties(scope, targetType, instance);
                scope.Nodes.Add(new EndNode());
                return scope.Nodes;
            }
        }

        /// <summary>
        /// Serializes the properties of an object.
        /// </summary>
        /// <param name="instance">The object of which to serialize properties.</param>
        /// <param name="uri">The uri at which the blueprint is to be stored. This is used to generate relative links within the blueprint.</param>
        /// <param name="rootName">The name of the root node.</param>
        /// <returns>The serialized nodes.</returns>
        public IReadOnlyList<INode> SerializeProperties(object instance, Uri uri = null, string rootName = null)
        {
            if (instance == null) throw new ArgumentNullException(nameof(instance));
            return SerializeProperties(instance.GetType(), instance, uri, rootName);
        }

        /// <summary>
        /// Serializes the properties of an object.
        /// </summary>
        /// <typeparam name="TargetType">The type of which to serialize properties.</typeparam>
        /// <param name="instance">The object of which to serialize properties.</param>
        /// <param name="uri">The uri at which the blueprint is to be stored. This is used to generate relative links within the blueprint.</param>
        /// <param name="rootName">The name of the root node.</param>
        /// <returns>The serialized nodes.</returns>
        public IReadOnlyList<INode> SerializeProperties<TargetType>(TargetType instance, Uri uri = null, string rootName = null)
        {
            return SerializeProperties(typeof(TargetType), instance, uri, rootName);
        }

        /// <summary>
        /// Serializes the properties of an object.
        /// </summary>
        /// <param name="targetType">The type of which to serialize properties.</param>
        /// <param name="stream">The stream to which to write.</param>
        /// <param name="instance">The object of which to serialize properties.</param>
        /// <param name="uri">The uri at which the blueprint is to be stored. This is used to generate relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which to store the blueprint.</param>
        /// <param name="rootName">The name of the root node.</param>
        public void SerializeProperties(Stream stream, Type targetType, object instance, Uri uri = null, MediaType mediaType = MediaType.Unknown, string rootName = null)
        {
            if (stream == null) throw new ArgumentNullException(nameof(stream));
            if (!stream.CanWrite) throw new ArgumentException("The provided stream must be writeable", nameof(stream));
            if (uri == null)
            {
                uri = new Uri("this:");
            }
            else if (mediaType == MediaType.Unknown)
            {
                mediaType = uri.GetMediaType();
            }

            using (IBlueprintWriter writer = BlueprintResolver.CreateWriter(stream, uri, mediaType))
            {
                SerializeProperties(writer, targetType, instance, rootName);
            }
        }

        /// <summary>
        /// Serializes the properties of an object.
        /// </summary>
        /// <param name="stream">The stream to which to write.</param>
        /// <param name="instance">The object of which to serialize properties.</param>
        /// <param name="uri">The uri at which the blueprint is to be stored. This is used to generate relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which to store the blueprint.</param>
        /// <param name="rootName">The name of the root node.</param>
        public void SerializeProperties(Stream stream, object instance, Uri uri = null, MediaType mediaType = MediaType.Unknown, string rootName = null)
        {
            if (instance == null) throw new ArgumentNullException(nameof(instance));
            SerializeProperties(stream, instance.GetType(), instance, uri, mediaType, rootName);
        }

        /// <summary>
        /// Serializes the properties of an object.
        /// </summary>
        /// <typeparam name="TargetType">The type of which to serialize properties.</typeparam>
        /// <param name="stream">The stream to which to write.</param>
        /// <param name="instance">The object of which to serialize properties.</param>
        /// <param name="uri">The uri at which the blueprint is to be stored. This is used to generate relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which to store the blueprint.</param>
        /// <param name="rootName">The name of the root node.</param>
        public void SerializeProperties<TargetType>(Stream stream, TargetType instance, Uri uri = null, MediaType mediaType = MediaType.Unknown, string rootName = null)
        {
            SerializeProperties(stream, typeof(TargetType), instance, uri, mediaType, rootName);
        }

        /// <summary>
        /// Serializes a multiple items of the same type.
        /// </summary>
        /// <typeparam name="ItemType">The type of items to serialize.</typeparam>
        /// <param name="writer">The writer to write to.</param>
        /// <param name="items">The items to serialize.</param>
        /// <param name="rootName">The name of the root node.</param>
        public void SerializeItems<ItemType>(IBlueprintWriter writer, IEnumerable<ItemType> items, string rootName = null)
        {
            if (writer == null) throw new ArgumentNullException(nameof(writer));
            if (items == null) throw new ArgumentNullException(nameof(items));
            foreach (INode node in SerializeItems(items, writer.DocumentUri, rootName))
            {
                writer.Write(node);
            }
        }

        /// <summary>
        /// Serializes a multiple items of the same type.
        /// </summary>
        /// <typeparam name="ItemType">The type of items to serialize.</typeparam>
        /// <param name="items">The items to serialize.</param>
        /// <param name="uri">The uri at which the blueprint is to be stored. This is used to generate relative links within the blueprint.</param>
        /// <param name="rootName">The name of the root node.</param>
        /// <returns>The serialized nodes.</returns>
        public IReadOnlyList<INode> SerializeItems<ItemType>(IEnumerable<ItemType> items, Uri uri = null, string rootName = null)
        {
            if (items == null) throw new ArgumentNullException(nameof(items));
            if (uri == null)
            {
                uri = new Uri("this:");
            }

            using (SerializationScope scope = new SerializationScope(this, uri))
            {
                string name = StartNode.GetValidNodeName(typeof(ItemType), "Item");
                scope.Nodes.Add(new StartNode(rootName ?? "BlueprintItems", TypeResolver.GetTypeName(typeof(ItemType[])), null, null));
                foreach (ItemType item in items)
                {
                    Serialize(scope, item, typeof(ItemType), name);
                }

                scope.Nodes.Add(new EndNode());
                return scope.Nodes;
            }
        }

        /// <summary>
        /// Serializes a multiple items of the same type.
        /// </summary>
        /// <typeparam name="ItemType">The type of items to serialize.</typeparam>
        /// <param name="stream">The stream to which to write.</param>
        /// <param name="items">The items to serialize.</param>
        /// <param name="uri">The uri at which the blueprint is to be stored. This is used to generate relative links within the blueprint.</param>
        /// <param name="mediaType">The media type with which to store the blueprint.</param>
        /// <param name="rootName">The name of the root node.</param>
        public void SerializeItems<ItemType>(Stream stream, IEnumerable<ItemType> items, Uri uri = null, MediaType mediaType = MediaType.Unknown, string rootName = null)
        {
            if (stream == null) throw new ArgumentNullException(nameof(stream));
            if (!stream.CanWrite) throw new ArgumentException("The provided stream must be writeable", nameof(stream));
            if (items == null) throw new ArgumentNullException(nameof(items));
            if (uri == null)
            {
                uri = new Uri("this:");
            }
            else if (mediaType == MediaType.Unknown)
            {
                mediaType = uri.GetMediaType();
            }

            using (IBlueprintWriter writer = BlueprintResolver.CreateWriter(stream, uri, mediaType))
            {
                SerializeItems(writer, items, rootName);
            }
        }
    }
}